# Copyright (c) Stanford University, The Regents of the University of
#               California, and others.
#
# All Rights Reserved.
#
# See Copyright-SimVascular.txt for additional details.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

''' The classes defined here are used to set options for the MeshSim mesh generator.
'''
from collections.abc import MutableSequence
from collections import OrderedDict

class MeshSimOptionTemplate(object):
    '''This class is used to store the templates defining the format of option parameters.
    '''
    GLOBAL_CURVATURE = {'curvature':float, 'absolute':bool} 
    GLOBAL_EDGE_SIZE = {'edge_size':float, 'absolute':bool} 
    GLOBAL_MIN_CURVATURE = {'min_curvature':float, 'absolute':bool} 
    # Local 'face_id' can be an integer face ID or string face name.
    LOCAL_CURVATURE = {'face_id':(str,int), 'curvature':float, 'absolute':bool} 
    LOCAL_EDGE_SIZE = {'face_id':(str,int), 'edge_size':float, 'absolute':bool} 
    LOCAL_MIN_CURVATURE = {'face_id':(str,int), 'min_curvature':float, 'absolute':bool} 

class MeshSimListOption(MutableSequence):
    ''' This class is used to store a list of dicts.

    The format of the dict elements are set using a template. Elements must 
    match the template in order to be added to the list.

    The dicts are typically used to store data for local meshing. For example, 
    the local curvature parameter for a face has the format

        {'face_id':1, 'curvature':1.0, 'absolute':True }
                              or
        {'face_id':'facename', 'curvature':1.0, 'absolute':True }
    '''

    def __init__(self, name, template):
        '''Initialize an object.

        Args:
            name (str): The name of the list.
            template (dict): The template describing the format of objects stored in the list.
        '''
        self.name = name 
        self.template = template
        self.dlist = []

    def __len__(self):
        return len(self.dlist)

    def __delitem__(self, index):
        self.dlist.__delitem__(index - 1)

    def insert(self, index, value):
        self.dlist.insert(index - 1, value)

    def __setitem__(self, index, value):
        self.dlist.__setitem__(index - 1, value)

    def __getitem__(self, index):
        return self.dlist.__getitem__(index - 1)

    def __str__(self):
        return '[ ' + ', '.join(map(str, self.dlist)) + ' ]'

    def append(self, value):
        '''Append a value to the list.
        '''
        if not self.check_value(value):
            raise ValueError('{0:s} must be {1:s}'.format(self.name, str(self.template)))
        self.dlist.append(value)

    def check_value(self, value):
        '''Check that a value added to the list matches the list template.

           Some values can have multiple types (e.g. face_id) so check that
           the input value is in a tuple of valid types (e.g. (str,int)). 
        '''
        if self.template.keys() != value.keys():
            return False
        for k,v in self.template.items():
            if type(v) is tuple:
                if type(value[k]) not in v:
                    return False
            elif type(value[k]) != v:
                return False
        return True

    def set_value(self, value):
        '''Initialize the value of the list.
        '''
        type_name = type(value).__name__
        if type_name != 'list':
            raise ValueError('{0:s} must be a list of {1:s}'.format(self.name,str(self.template)))
        for item in value:
            if not self.check_value(item):
                raise ValueError('{0:s} must be {1:s}'.format(self.name, str(self.template)))
            self.dlist.append(item)
        return True

class MeshSimOptions(object):
    '''The MeshSimOptions class is used to store option parameters for the MeshSim mesh generator.

    Option parameters are of four types: numeric, bool, dict and list of dicts. The parameter
    values can only be set to their predefined type. The @setter decorator is used to check 
    for valid parameter values. 
    '''

    # Define certain parameter default values.
    SURFACE_OPTIMIZATION_DEFAULT = 1
    SURFACE_SMOOTHING_DEFAULT = 3
    VOLUME_OPTIMIZATION_DEFAULT = 1
    VOLUME_SMOOTHING_DEFAULT = 3

    def __init__(self, global_edge_size=None, surface_mesh_flag=True, volume_mesh_flag=True):
        self._global_curvature = None
        self._global_min_curvature = None
        self._global_edge_size = global_edge_size

        self._local_edge_size = MeshSimListOption("local_edge_size", MeshSimOptionTemplate.LOCAL_EDGE_SIZE)
        self._local_curvature = MeshSimListOption("local_curvature", MeshSimOptionTemplate.LOCAL_CURVATURE)
        self._local_min_curvature = MeshSimListOption("local_min_curvature", MeshSimOptionTemplate.LOCAL_MIN_CURVATURE)

        self._surface_mesh_flag = surface_mesh_flag
        self._surface_optimization = MeshSimOptions.SURFACE_OPTIMIZATION_DEFAULT
        self._surface_smoothing = MeshSimOptions.SURFACE_SMOOTHING_DEFAULT

        self._volume_mesh_flag = volume_mesh_flag
        self._volume_optimization = MeshSimOptions.VOLUME_OPTIMIZATION_DEFAULT 
        self._volume_smoothing = MeshSimOptions.VOLUME_SMOOTHING_DEFAULT 

    def check_value(self, template, value):
        '''Check that a value matches the given template.
        '''
        if template.keys() != value.keys():
            return False
        for k,v in template.items():
            if type(value[k]) != v:
                return False
        return True

    @property
    def global_curvature(self):
        return self._global_curvature

    @global_curvature.setter
    def global_curvature(self, value):
        if (not self.check_value(MeshSimOptionTemplate.GLOBAL_CURVATURE, value)):
            raise ValueError('global_curvature must be a ' + str(MeshSimOptionTemplate.GLOBAL_CURVATURE))
        self._global_curvature = value

    @property
    def global_min_curvature(self):
        return self._global_min_curvature

    @global_min_curvature.setter
    def global_min_curvature(self, value):
        if (not self.check_value(MeshSimOptionTemplate.GLOBAL_MIN_CURVATURE, value)):
            raise ValueError('global_min_curvature must be a ' + str(MeshSimOptionTemplate.GLOBAL_MIN_CURVATURE))
        self._global_min_curvature = value

    @property
    def global_edge_size(self):
        return self._global_edge_size

    @global_edge_size.setter
    def global_edge_size(self, value):
        if (not self.check_value(MeshSimOptionTemplate.GLOBAL_EDGE_SIZE, value)):
            raise ValueError('global_edge_size must be a ' + str(MeshSimOptionTemplate.GLOBAL_EDGE_SIZE))
        self._global_edge_size = value

    @property
    def local_curvature(self):
        return self._local_curvature

    @local_curvature.setter
    def local_curvature(self, value):
        self._local_curvature.set_value(value)

    @property
    def local_min_curvature(self):
        return self._local_min_curvature

    @local_min_curvature.setter
    def local_min_curvature(self, value):
        self._local_min_curvature.set_value(value)

    @property
    def local_edge_size(self):
        return self._local_edge_size

    @local_edge_size.setter
    def local_edge_size(self, value):
        self._local_edge_size.set_value(value)

    @property
    def surface_mesh_flag(self):
        return self._surface_mesh_flag

    @surface_mesh_flag.setter
    def surface_mesh_flag(self, value):
        if type(value).__name__ != "bool":
            raise ValueError('surface_mesh_flag must be a bool.')
        self._surface_mesh_flag = value

    @property
    def surface_optimization(self):
        return self._surface_mesh_flag

    @surface_optimization.setter
    def surface_optimization(self, value):
        if type(value).__name__ != "int":
            raise ValueError('surface_optimization must be an int.')
        if value <= 0:
            raise ValueError('surface_optimization must be an int > 0.')
        self._surface_optimization = value

    @property
    def surface_smoothing(self):
        return self._surface_smoothing

    @surface_smoothing.setter
    def surface_smoothing(self, value):
        if type(value).__name__ != "int":
            raise ValueError('surface_smoothing must be an int.')
        if value <= 0:
            raise ValueError('surface_smoothing must be an int > 0.')
        self._surface_smoothing = value

    @property
    def volume_mesh_flag(self):
        return self._volume_mesh_flag

    @volume_mesh_flag.setter
    def volume_mesh_flag(self, value):
        if type(value).__name__ != "bool":
            raise ValueError('volume_mesh_flag must be a bool.')
        self._volume_mesh_flag = value

    @property
    def volume_optimization(self):
        return self._volume_mesh_flag

    @volume_optimization.setter
    def volume_optimization(self, value):
        if type(value).__name__ != "int":
            raise ValueError('volume_optimization must be an int.')
        if value <= 0:
            raise ValueError('volume_optimization must be an int > 0.')
        self._volume_optimization = value

    @property
    def volume_smoothing(self):
        return self._volume_smoothing

    @volume_smoothing.setter
    def volume_smoothing(self, value):
        if type(value).__name__ != "int":
            raise ValueError('volume_smoothing must be an int.')
        if value <= 0:
            raise ValueError('volume_smoothing must be an int > 0.')
        self._volume_smoothing = value

    def get_values(self):
        '''Get the option values as a dict.
        '''
        values = OrderedDict()
        for name in dir(self):
            if name[0] != '_' or name[1] == '_':
                continue
            values[name[1:]] = str(getattr(self, name))
        return values 

